1   /*
2    * Copyright (C) 2012 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.google.common.base;
18  
19  import com.google.caliper.BeforeExperiment;
20  import com.google.caliper.Benchmark;
21  import com.google.caliper.Param;
22  
23  import java.util.Arrays;
24  import java.util.Iterator;
25  
26  /**
27   * Benchmarks {@link Joiner} against some common implementations of delimiter-based
28   * string joining.
29   *
30   * @author Adomas Paltanavicius
31   */
32  public class JoinerBenchmark {
33  
34    private static final String DELIMITER_STRING = ",";
35    private static final char DELIMITER_CHARACTER = ',';
36  
37    private static final Joiner JOINER_ON_STRING = Joiner.on(DELIMITER_STRING);
38    private static final Joiner JOINER_ON_CHARACTER = Joiner.on(DELIMITER_CHARACTER);
39  
40    @Param({"3", "30", "300"}) int count;
41    @Param({"0", "1", "16", "32", "100"}) int componentLength;
42  
43    private Iterable<String> components;
44  
45    @BeforeExperiment
46    void setUp() {
47      String component = Strings.repeat("a", componentLength);
48      String[] raw = new String[count];
49      Arrays.fill(raw, component);
50      components = Arrays.asList(raw);
51    }
52  
53    /**
54     * {@link Joiner} with a string delimiter.
55     */
56    @Benchmark int joinerWithStringDelimiter(int reps) {
57      int dummy = 0;
58      for (int i = 0; i < reps; i++) {
59        dummy ^= JOINER_ON_STRING.join(components).length();
60      }
61      return dummy;
62    }
63  
64    /**
65     * {@link Joiner} with a character delimiter.
66     */
67    @Benchmark int joinerWithCharacterDelimiter(int reps) {
68      int dummy = 0;
69      for (int i = 0; i < reps; i++) {
70        dummy ^= JOINER_ON_CHARACTER.join(components).length();
71      }
72      return dummy;
73    }
74  
75    /**
76     * Mimics what the {@link Joiner} class does internally when no extra options like
77     * ignoring {@code null} values are used.
78     */
79    @Benchmark int joinerInlined(int reps) {
80      int dummy = 0;
81      for (int i = 0; i < reps; i++) {
82        StringBuilder sb = new StringBuilder();
83        Iterator<String> iterator = components.iterator();
84        if (iterator.hasNext()) {
85          sb.append(iterator.next().toString());
86          while (iterator.hasNext()) {
87            sb.append(DELIMITER_STRING);
88            sb.append(iterator.next());
89          }
90        }
91        dummy ^= sb.toString().length();
92      }
93      return dummy;
94    }
95  
96    /**
97     * Only appends delimiter if the accumulated string is non-empty.
98     * Note: this isn't a candidate implementation for Joiner since it fails on leading
99     * empty components.
100    */
101   @Benchmark int stringBuilderIsEmpty(int reps) {
102     int dummy = 0;
103     for (int i = 0; i < reps; i++) {
104       StringBuilder sb = new StringBuilder();
105       for (String comp : components) {
106         if (sb.length() > 0) {
107           sb.append(DELIMITER_STRING);
108         }
109         sb.append(comp);
110       }
111       dummy ^= sb.toString().length();
112     }
113     return dummy;
114   }
115 
116   /**
117    * Similar to the above, but keeps a boolean flag rather than checking for the string
118    * accumulated so far being empty. As a result, it does not have the above-mentioned bug.
119    */
120   @Benchmark int booleanIfFirst(int reps) {
121     int dummy = 0;
122     for (int i = 0; i < reps; i++) {
123       StringBuilder sb = new StringBuilder();
124       boolean append = false;
125       for (String comp : components) {
126         if (append) {
127           sb.append(DELIMITER_STRING);
128         }
129         sb.append(comp);
130         append = true;
131       }
132       dummy ^= sb.toString().length();
133     }
134     return dummy;
135   }
136 
137   /**
138    * Starts with an empty delimiter and changes to the desired value at the end of the
139    * iteration.
140    */
141   @Benchmark int assignDelimiter(int reps) {
142     int dummy = 0;
143     for (int i = 0; i < reps; i++) {
144       StringBuilder sb = new StringBuilder();
145       String delim = "";
146       for (String comp : components) {
147         sb.append(delim);
148         sb.append(comp);
149         delim = DELIMITER_STRING;
150       }
151       dummy ^= sb.toString().length();
152     }
153     return dummy;
154   }
155 
156   /**
157    * Always append the delimiter after the component, and in the very end shortens the buffer
158    * to get rid of the extra trailing delimiter.
159    */
160   @Benchmark int alwaysAppendThenBackUp(int reps) {
161     int dummy = 0;
162     for (int i = 0; i < reps; i++) {
163       StringBuilder sb = new StringBuilder();
164       for (String comp : components) {
165         sb.append(comp);
166         sb.append(DELIMITER_STRING);
167       }
168       if (sb.length() > 0) {
169         sb.setLength(sb.length() - DELIMITER_STRING.length());
170       }
171       dummy ^= sb.toString().length();
172     }
173     return dummy;
174   }
175 }